﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq.Expressions;
using System.Reflection;
using Framework.Attributes;
using Framework.Extensions;
using Framework.Metadata;
using System.Linq;
using Framework.Sql;

namespace Framework.Model
{
    public class Repository
    {
        private readonly IConnectionConfiguration _connectionConfiguration;
        private readonly ICache _cache;

        public Repository(IConnectionConfiguration connectionConfiguration, ICache cache)
        {
            _connectionConfiguration = connectionConfiguration;
            _cache = cache;
        }

        public string ConnectionString
        {
            get { return _connectionConfiguration.ConnectionString; }
        }

        private static PropertyInfo GetImplementationOf(PropertyInfo baseProperty, Type implementationType)
        {
            return implementationType.GetProperty(baseProperty.Name);
        }

        private static bool IsImplementationOf(PropertyInfo baseProperty, PropertyInfo implementationProperty)
        {
            return baseProperty.Name.Equals(implementationProperty.Name)
                && baseProperty.ReflectedType.IsAssignableFrom(implementationProperty.ReflectedType);
        }

        private void Update(IId entity)
        {
            var idProperty = typeof (IId).GetProperties().Single();
            idProperty = entity.GetType().GetProperties().Single(p => p.Name == idProperty.Name);
            var idMetadata = (ColumnPropertyMetadata)PropertyMetadataBase.FromProperty(idProperty);

            var typeMetadata = TypeMetadata.FromType(entity.GetType());
            var sqlBuilder = new UpdateSqlBuilder(ConnectionString, typeMetadata.TableName, idMetadata.ColumnName, entity.Id);

            var propertyMetadatas = entity.GetType()
                .GetProperties()
                .Select(PropertyMetadataBase.FromProperty)
                .Where(p => p != null)
                .Where(metadata => !IsImplementationOf(typeof(IId).GetProperties().Single(), metadata.PropertyInfo))
                .ToList();

            foreach (var modifier in propertyMetadatas.OfType<IInsertSqlBuilderModifier>())
                modifier.ModifySql(sqlBuilder, entity);

            sqlBuilder.ExecuteSql();
        }

        public void Delete<T>(Expression<Func<T, bool>> filter)
        {
            var typeMetadata = TypeMetadata.FromType(typeof(T));
            var sqlBuilder = new DeleteSqlBuilder(ConnectionString, typeMetadata.TableName);
            new ExpressionToSqlGenerator().GetSql(sqlBuilder, filter);
            sqlBuilder.ExecuteSql();
        }

        public void Save(object entity)
        {
            Save(entity, null);
        }

        private void Save(object entity, Action<InsertSqlBuilder> insertAction)
        {
            var typeMetadata = TypeMetadata.FromType(entity.GetType());
            var sqlBuilder = new InsertSqlBuilder(ConnectionString, typeMetadata.TableName);

            var propertyMetadatas = entity.GetType()
                .GetProperties()
                .Select(PropertyMetadataBase.FromProperty)
                .Where(p => p != null)
                .Where(metadata => !IsImplementationOf(typeof(IId).GetProperties().Single(), metadata.PropertyInfo))
                .ToList();

            var iid = entity as IId;
            if (iid != null && iid.Id > 0)
            {
                Update(iid);

                foreach (var propertyMetadata in propertyMetadatas)
                    propertyMetadata.SaveObject(Save, entity, Delete);
                return;
            }

            foreach (var modifier in propertyMetadatas.OfType<IInsertSqlBuilderModifier>())
                modifier.ModifySql(sqlBuilder, entity);

            if (insertAction != null) insertAction(sqlBuilder);

            var newId = sqlBuilder.ExecuteSql();
            ((IId) entity).Id = newId;

            foreach (var propertyMetadata in propertyMetadatas)
                propertyMetadata.SaveObject(Save, entity);

            AddItemToListCache(entity);
        }

        private void Delete(Type type, PropertyInfo propertyInfo, int id, IEnumerable<int> idsToNotDelete)
        {
            var idProperty = typeof (IId).GetProperties().Single();
            idProperty = type.GetProperties().Single(p => p.Name == idProperty.Name);

            var typeMetadata = TypeMetadata.FromType(type);
            var propertyMetadata = (ColumnPropertyMetadata) PropertyMetadataBase.FromProperty(propertyInfo);
            var parentColumnName = propertyMetadata.ColumnName;

            propertyMetadata = (ColumnPropertyMetadata) PropertyMetadataBase.FromProperty(idProperty);
            var columnName = propertyMetadata.ColumnName;
            var sqlBuilder = new DeleteXrefChildrenSqlBuilder(ConnectionString, typeMetadata.TableName, parentColumnName, id, columnName, idsToNotDelete);
            sqlBuilder.ExecuteSql();
        }

        public IEnumerable Find(Type type, LambdaExpression expression)
        {
            return Find(type, expression, null);
        }

        public IEnumerable Find(Type type, LambdaExpression expression, int pageNumber, int pageCount, params SortColumn[] sortColumns)
        {
            return Find(type, expression, (builder, subType) =>
                                              {
                                                  builder.PageNumber = pageNumber;
                                                  builder.PageCount = pageCount;
                                                  if (sortColumns == null) return;
                                                  foreach (var column in sortColumns)
                                                  {
                                                      var property = subType.GetProperty(column.Column);
                                                      var propertyMetadata = PropertyMetadataBase.FromProperty(property) as ColumnPropertyMetadata;
                                                      if (propertyMetadata == null) continue;
                                                      builder.AddSortColumn(propertyMetadata.ColumnName, column.Order.Equals("desc", StringComparison.CurrentCultureIgnoreCase));
                                                  }
                                              });
        }

        private static SelectSqlBuilder GetIdSqlBuilder(string connectionString, Type type)
        {
            var typeMetadata = TypeMetadata.FromType(type);
            var idProperty = (ColumnPropertyMetadata) PropertyMetadataBase.FromProperty(type.GetProperty("Id"));
            var sqlBuilder = new SelectSqlBuilder(connectionString, typeMetadata.TableName);
            sqlBuilder.AddSelectColumn(idProperty.ColumnName);
            sqlBuilder.AddSelectColumn("'" + type.Name + "'");
            return sqlBuilder;
        }

        public int Count(Type type)
        {
            if (type.IsInterface)
            {
                var subTypes = type.Assembly.GetTypes().Where(t => type.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract).ToList();
                return subTypes.Aggregate(0, (i, t) => i + Count(t));
            }

            var typeMetadata = TypeMetadata.FromType(type);
            var sqlBuilder = new CountSqlBuilder(ConnectionString, typeMetadata.TableName);
            return sqlBuilder.Count();
        }

        private IEnumerable Find(Type type, LambdaExpression expression, Action<SelectSqlBuilder, Type> selectAction)
        {
            var typeMetadata = TypeMetadata.FromType(type);

            var sqlBuilder = new SelectSqlBuilder(ConnectionString, typeMetadata.TableName);

            if (type.IsInterface) return FindInterface(type, selectAction);

            if (expression != null) new ExpressionToSqlGenerator().GetSql(sqlBuilder, expression);

            var propertyMetadatas = type
                .GetProperties()
                .Select(PropertyMetadataBase.FromProperty)
                .Where(p => p != null)
                .ToList();

            foreach (var modifier in propertyMetadatas.OfType<ISelectSqlBuilderModifier>())
                modifier.ModifySql(sqlBuilder);

            if (selectAction != null) selectAction(sqlBuilder, type);

            return sqlBuilder.ExecuteSql(reader =>
                                                    {
                                                        var t = Activator.CreateInstance(type);
                                                        UpdateObject(t, propertyMetadatas, reader);
                                                        AddItemToCache(type, (IId) t);
                                                        return t;
                                                    })
                                                    .ToListOfType(type);
        }

        private IEnumerable FindInterface(Type type, Action<SelectSqlBuilder, Type> selectAction)
        {
            var subTypes = type.Assembly.GetTypes().Where(t => type.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract).ToList();
            var builders = subTypes
                .Select(t =>
                            {
                                var builder = GetIdSqlBuilder(ConnectionString, t);
                                selectAction(builder, t);
                                return builder;
                            })
                .ToList();

            builders.First().Union(builders.Skip(1).ToArray());
            builders.First().PageSortColumn = builders.First().ColumnNames.First();

            var ids = builders
                .First()
                .ExecuteSql(reader => new {Id = (int) reader[0], TableName = reader[1].ToString()})
                .ToList();

            var typeLookup = subTypes.ToDictionary(t => t.Name);
            return ids
                .GroupBy(a => a.TableName)
                .SelectMany(g =>
                                {
                                    var items = g.Select(a => a.Id).ToList();

                                    var parameter = Expression.Parameter(typeLookup[g.Key], "x");
                                    var methodInfo = items.GetType().GetMethods().Single(m => m.Name == "Contains" && m.GetParameters().Count() == 1);
                                    var memberExpression = Expression.MakeMemberAccess(parameter, typeLookup[g.Key].GetProperty("Id"));
                                    var call = Expression.Call(Expression.Constant(items), methodInfo, memberExpression);
                                    var lambda = Expression.Lambda(call, parameter);

                                    return Find(typeLookup[g.Key], lambda).Cast <IId>();
                                })
                .OrderBy(id => ids.FindIndex(a => a.Id == id.Id && a.TableName.Equals(TypeMetadata.FromType(id.GetType()).TableName, StringComparison.CurrentCultureIgnoreCase)))
                .ToListOfType(type);
        }

        private static bool TypeIsListCached(Type type)
        {
            return type.IsDefined(typeof (RepositoryCacheAttribute), false);
        }

        private void AddItemToCache(Type type, IId entity)
        {
            if (_cache == null) return;
            if (!TypeIsListCached(type)) return;
            _cache[type.Name + "_" + entity.Id] = entity;
        }

        private IId GetItemFromCache(Type type, int id)
        {
            if (_cache == null) return null;
            return (IId) _cache[type.Name + "_" + id];
        }

        public void ClearCache(Type type)
        {
            _cache[type.Name] = null;
        }

        private void AddItemToListCache(object listItem)
        {
            var list = GetListFromCache(listItem.GetType());
            if (list == null) return;
            list.GetType().GetMethod("Add").Invoke(list, new[] {listItem});
        }

        private void AddListToCache(Type type, object listResult)
        {
            if (_cache == null) return;
            if (!TypeIsListCached(type)) return;
            _cache[type.Name] = listResult;
        }

        private IEnumerable GetListFromCache(Type type)
        {
            if (_cache == null) return null;
            return (IEnumerable) _cache[type.Name];
        }

        public IEnumerable<T> FindAll<T>()
        {
            var cachedList = GetListFromCache(typeof (T));
            if (cachedList != null) return cachedList.Cast<T>();

            var results = Find(typeof (T), null).Cast<T>();
            AddListToCache(typeof(T), results.ToList());
            return results;
        }

        public IEnumerable<T> Find<T>(Expression<Func<T, bool>> expression)
        {
            return Find(typeof (T), expression).Cast<T>();
        }

        public T Get<T>(int id)
        {
            var cachedItem = GetItemFromCache(typeof (T), id);
            if (cachedItem != null) return (T) cachedItem;

            var idProperty = GetImplementationOf(typeof (IId).GetProperties().Single(), typeof(T));

            var parameter = Expression.Parameter(typeof (T));
            var memberExpression = Expression.MakeMemberAccess(parameter, idProperty);
            var equalExpression = Expression.Equal(memberExpression, Expression.Constant(id));
            var lambda = Expression.Lambda(equalExpression, parameter);

            return Find(typeof(T), lambda).Cast<T>().SingleOrDefault();
        }

        private void UpdateObject(object o, IEnumerable<PropertyMetadataBase> propertyMetadatas, IDataRecord reader)
        {
            foreach (var propertyMetadata in propertyMetadatas)
            {
                propertyMetadata.UpdateObject(o, reader, Find);
            }
        }
    }
}